#!/usr/bin/perl -w

#=======================================================================
# smsum
# File ID: ac639b66-f942-11dd-8b88-0001805bf4b1
# Generates smsum hashes
#
# Character set: UTF-8
# ©opyleft 2008– Øyvind A. Holm <sunny@sunbase.org>
# License: GNU General Public License version 2 or later, see end of 
# file for legal stuff.
#=======================================================================

use strict;
use Getopt::Long;

$| = 1;

our $Debug = 0;

our %Opt = (

    'debug' => 0,
    'help' => 0,
    'verbose' => 0,
    'version' => 0,
    'with-mtime' => 0,

);

our $progname = $0;
$progname =~ s/^.*\/(.*?)$/$1/;
our $VERSION = "0.00";
my $exit_val = 0;

Getopt::Long::Configure("bundling");
GetOptions(

    "debug" => \$Opt{'debug'},
    "help|h" => \$Opt{'help'},
    "verbose|v+" => \$Opt{'verbose'},
    "version" => \$Opt{'version'},
    "with-mtime|m" => \$Opt{'with-mtime'},

) || die("$progname: Option error. Use -h for help.\n");

$Opt{'debug'} && ($Debug = 1);
$Opt{'help'} && usage(0);
if ($Opt{'version'}) {
    print_version();
    exit(0);
}

if ($#ARGV >= 0) {
    for (@ARGV) {
        print(process_file($_));
    }
} else {
    print(process_file("-"));
}

exit($exit_val);

sub process_file {
    # {{{
    my $Filename = shift;
    my $Retval = "";
    my %Sum = ();
    D("process_file('$Filename')");
    my $use_stdin = ($Filename eq "-") ? 1 : 0;
    my @stat_array = ();
    my $displayed_filename = $Filename;
    my $tmpfile = "/tmp/$progname." . time . ".$$.tmp";
    if ($use_stdin) {
        open(my $tmpfp, '>', $tmpfile) or die("$progname: $tmpfile: Cannot create tempfile: $!\n");
        unless (print($tmpfp join('', <STDIN>))) {
            my $errmsg = $!;
            close($tmpfp);
            unlink($tmpfile);
            die("$progname: $tmpfile: Cannot write to tempfile: $errmsg\n");
        }
        close($tmpfp);
        $Filename = $tmpfile;
    }
    if (@stat_array = stat($Filename)) {
        my ($Dev, $Inode, $Mode, $Nlinks, $Uid, $Gid, $Rdev, $Size,
            $Atime, $Mtime, $Ctime, $Blksize, $Blocks) = @stat_array;
        if ($use_stdin || -f $Filename) {
            local *FP;
            if ($use_stdin || open(FP, "<", $Filename)) {
                msg(2, sprintf("Reading %s...", safe_tab($Filename)));
                $Sum{'sha1'} = sha1sum($Filename);
                $Sum{'md5'} = md5sum($Filename);
                $Retval =
                    $Sum{'sha1'} . "-" .
                    $Sum{'md5'} . "-" .
                    $Size . (
                        $use_stdin && !$Opt{'with-mtime'}
                            ? ""
                            : "\t" . safe_tab($displayed_filename) . (
                                  $Opt{'with-mtime'}
                                      ? "\t" . sec_to_string($Mtime)
                                      : ""
                              )
                    )
                    . "\n";
            } else {
                warn("$progname: $Filename: Cannot read file\n");
                $exit_val = 1;
            }
        } else {
            msg(1, "$Filename: Ignoring non-file");
        }
    } else {
        warn("$progname: $Filename: Cannot read file status\n");
        $exit_val = 1;
    }
    unlink($tmpfile);
    return($Retval);
    # }}}
} # process_file()

sub sha1sum {
    my $file = shift;
    my $retval = `sha1sum "$file"`;
    $retval =~ s/^(\S+)\b.*/$1/s;
    return($retval);
}

sub md5sum {
    my $file = shift;
    chomp(my $retval = `md5sum "$file"`);
    $retval =~ s/^(\S+)\b.*/$1/s;
    return($retval);
}

sub safe_tab {
    # {{{
    my $Str = shift;
    $Str =~ s/\\/\\\\/gs;
    $Str =~ s/\n/\\n/gs;
    $Str =~ s/\r/\\r/gs;
    $Str =~ s/\t/\\t/gs;
    return($Str);
    # }}}
} # safe_tab()

sub sec_to_string {
    # Convert seconds since 1970 to "yyyy-mm-ddThh:mm:ssZ" {{{
    my ($Seconds) = shift;

    my @TA = gmtime($Seconds);
    my($DateString) = sprintf("%04u-%02u-%02uT%02u:%02u:%02uZ",
                              $TA[5]+1900, $TA[4]+1, $TA[3],
                              $TA[2], $TA[1], $TA[0]);
    return($DateString);
    # }}}
} # sec_to_string()

sub print_version {
    # Print program version {{{
    print("$progname v$VERSION\n");
    # }}}
} # print_version()

sub usage {
    # Send the help message to stdout {{{
    my $Retval = shift;

    if ($Opt{'verbose'}) {
        print("\n");
        print_version();
    }
    print(<<END);

Usage: $progname [options] [file [files [...]]]

The program is based on the same principle as md5sum(1) and sha1sum(1), 
but combines the two hashes and also includes the file size:

  [SHA1][-][MD5][-][SIZE][\\t][FILENAME][\\n]

or if the --with-mtime option is used:

  [SHA1][-][MD5][-][SIZE][\\t][FILENAME][\\t][MTIME][\\n]

The reason for this approach, is that both hashing algoritms are well 
known and widely used. Both algorithms are good enough for everyday 
content verification, but at least the MD5 algorithm is vulnerable to 
intentional collisions. Instead of inventing new algorithms which has to 
earn trust over the years, combining the two well examined algorithms 
and adding the size of the file will make a smsum hash collision much 
harder.

If no filenames are specified on the command line, stdin is used.

Special characters in filenames are escaped this way:

Horizontal Tab  (0x09): \\t
Line feed       (0x0a): \\n
Carriage return (0x0d): \\r
Backslash  ('\\', 0x5c): \\\\

Options:

  -h, --help
    Show this help.
  -m, --with-mtime
    Also include file modification time at the end of every line. The 
    date uses the UTC timezone and has the format 
    "yyyy-mm-ddThh:mm:ssZ". If stdin is read, the current time is used.
  -v, --verbose
    Increase level of verbosity. Can be repeated.
  --version
    Print version information.
  --debug
    Print debugging messages.

END
    exit($Retval);
    # }}}
} # usage()

sub msg {
    # Print a status message to stderr based on verbosity level {{{
    my ($verbose_level, $Txt) = @_;

    if ($Opt{'verbose'} >= $verbose_level) {
        print(STDERR "$progname: $Txt\n");
    }
    # }}}
} # msg()

sub D {
    # Print a debugging message {{{
    $Debug || return;
    my @call_info = caller;
    chomp(my $Txt = shift);
    my $File = $call_info[1];
    $File =~ s#\\#/#g;
    $File =~ s#^.*/(.*?)$#$1#;
    print(STDERR "$File:$call_info[2] $$ $Txt\n");
    return("");
    # }}}
} # D()

__END__

# Plain Old Documentation (POD) {{{

=pod

=head1 NAME



=head1 SYNOPSIS

 [options] [file [files [...]]]

=head1 DESCRIPTION



=head1 OPTIONS

=over 4

=item B<-h>, B<--help>

Print a brief help summary.

=item B<-v>, B<--verbose>

Increase level of verbosity. Can be repeated.

=item B<--version>

Print version information.

=item B<--debug>

Print debugging messages.

=back

=head1 BUGS



=head1 AUTHOR

Made by Øyvind A. Holm S<E<lt>sunny@sunbase.orgE<gt>>.

=head1 COPYRIGHT

Copyleft © Øyvind A. Holm E<lt>sunny@sunbase.orgE<gt>
This is free software; see the file F<COPYING> for legalese stuff.

=head1 LICENCE

This program is free software: you can redistribute it and/or modify it 
under the terms of the GNU General Public License as published by the 
Free Software Foundation, either version 2 of the License, or (at your 
option) any later version.

This program is distributed in the hope that it will be useful, but 
WITHOUT ANY WARRANTY; without even the implied warranty of 
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along 
with this program.
If not, see L<http://www.gnu.org/licenses/>.

=head1 SEE ALSO

=cut

# }}}

# vim: set fenc=UTF-8 ft=perl fdm=marker ts=4 sw=4 sts=4 et fo+=w :
